/*
 * Copyright 2012-2022 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.springframework.boot.builder;

import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.concurrent.atomic.AtomicBoolean;

import org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory;
import org.springframework.beans.factory.support.BeanNameGenerator;
import org.springframework.boot.ApplicationContextFactory;
import org.springframework.boot.Banner;
import org.springframework.boot.BootstrapRegistry;
import org.springframework.boot.BootstrapRegistryInitializer;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.WebApplicationType;
import org.springframework.boot.convert.ApplicationConversionService;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextInitializer;
import org.springframework.context.ApplicationListener;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.env.Environment;
import org.springframework.core.io.ResourceLoader;
import org.springframework.core.metrics.ApplicationStartup;
import org.springframework.util.StringUtils;

/**
 * {@link SpringApplication} 和 {@link ApplicationContext} 实例的构建器，提供方便的流式 API 和上下文层级支持。
 * 上下文层级的简单示例：
 *
 * <pre class="code">
 * new SpringApplicationBuilder(ParentConfig.class).child(ChildConfig.class).run(args);
 * </pre>
 *
 * 另一个常见用例是设置激活的 profile 和默认属性，以配置应用的环境：
 *
 * <pre class="code">
 * new SpringApplicationBuilder(Application.class).profiles("server")
 * 		.properties("transport=local").run(args);
 * </pre>
 *
 * <p>
 * 如果你的需求比较简单，建议使用 SpringApplication 中的静态便捷方法。
 *
 * @author Dave Syer
 * @author Andy Wilkinson
 * @since 1.0.0
 * @see SpringApplication
 */
public class SpringApplicationBuilder {

	private final SpringApplication application;

	private ConfigurableApplicationContext context;

	private SpringApplicationBuilder parent;

	private final AtomicBoolean running = new AtomicBoolean();

	private final Set<Class<?>> sources = new LinkedHashSet<>();

	private final Map<String, Object> defaultProperties = new LinkedHashMap<>();

	private ConfigurableEnvironment environment;

	private Set<String> additionalProfiles = new LinkedHashSet<>();

	private boolean registerShutdownHookApplied;

	private boolean configuredAsChild = false;

	public SpringApplicationBuilder(Class<?>... sources) {
		this(null, sources);
	}

	public SpringApplicationBuilder(ResourceLoader resourceLoader, Class<?>... sources) {
		this.application = createSpringApplication(resourceLoader, sources);
	}

	/**
	 * 从指定的 sources 创建一个新的 {@link SpringApplication} 实例。子类可以重写此方法，
	 * 以提供自定义的 {@link SpringApplication} 子类。
	 * @param sources 源类
	 * @return {@link SpringApplication} 实例
	 * @since 1.1.0
	 * @deprecated 从 2.6.0 开始弃用，计划在 3.0.0 移除，建议使用
	 * {@link #createSpringApplication(ResourceLoader, Class...)} 代替
	 */
	@Deprecated
	protected SpringApplication createSpringApplication(Class<?>... sources) {
		return new SpringApplication(sources);
	}

	/**
	 * 使用给定的 {@link ResourceLoader} 和指定的 sources 创建一个新的 {@link SpringApplication} 实例。
	 * 子类可以重写此方法，以提供自定义的 {@link SpringApplication} 子类。
	 * @param resourceLoader 资源加载器（可以为 null）
	 * @param sources 源类
	 * @return {@link SpringApplication} 实例
	 * @since 2.6.0
	 */
	protected SpringApplication createSpringApplication(ResourceLoader resourceLoader, Class<?>... sources) {
		return new SpringApplication(resourceLoader, sources);
	}

	/**
	 * 获取当前的应用上下文。
	 * @return 当前的应用上下文（如果还未启动则返回 null）
	 */
	public ConfigurableApplicationContext context() {
		return this.context;
	}

	/**
	 * 获取当前的应用实例。
	 * @return 当前的应用实例（永不为 null）
	 */
	public SpringApplication application() {
		return this.application;
	}

	/**
	 * 使用提供的命令行参数创建一个应用上下文（以及其父上下文，如果有指定的话）。
	 * 如果父上下文尚未启动，则先使用相同的参数启动父上下文。
	 * @param args 命令行参数
	 * @return 从当前状态创建的应用上下文
	 */
	public ConfigurableApplicationContext run(String... args) {
		if (this.running.get()) {
			// 如果已经创建，则直接返回现有的上下文
			return this.context;
		}
		configureAsChildIfNecessary(args);
		if (this.running.compareAndSet(false, true)) {
			synchronized (this.running) {
				// 如果尚未运行，复制配置源然后运行
				this.context = build().run(args);
			}
		}
		return this.context;
	}

	private void configureAsChildIfNecessary(String... args) {
		if (this.parent != null && !this.configuredAsChild) {
			this.configuredAsChild = true;
			if (!this.registerShutdownHookApplied) {
				this.application.setRegisterShutdownHook(false);
			}
			initializers(new ParentContextApplicationContextInitializer(this.parent.run(args)));
		}
	}

	/**
	 * 返回一个完全配置好的 {@link SpringApplication}，准备运行。
	 * @return 完全配置好的 {@link SpringApplication}
	 */
	public SpringApplication build() {
		return build(new String[0]);
	}

	/**
	 * 返回一个完全配置好的 {@link SpringApplication}，准备运行。任何已配置的父应用
	 * 都将使用给定的 {@code args} 运行。
	 * @param args 父应用的参数
	 * @return 完全配置好的 {@link SpringApplication}
	 */
	public SpringApplication build(String... args) {
		configureAsChildIfNecessary(args);
		this.application.addPrimarySources(this.sources);
		return this.application;
	}

	/**
	 * 使用提供的资源创建一个子应用。默认的参数和环境会复制到子应用，
	 * 但其他一切都是干净的全新状态。
	 * @param sources 应用的资源（Spring 配置）
	 * @return 子应用构建器
	 */
	public SpringApplicationBuilder child(Class<?>... sources) {
		SpringApplicationBuilder child = new SpringApplicationBuilder();
		child.sources(sources);

		// 从父应用复制环境相关配置到子应用
		child.properties(this.defaultProperties).environment(this.environment)
				.additionalProfiles(this.additionalProfiles);
		child.parent = this;

		// 如果启用了嵌入式 Web 服务器，无法支持以 Web 上下文作为父上下文，
		// 因为 Servlet 无法在正确的生命周期点初始化。
		web(WebApplicationType.NONE);

		// 可能不需要多个启动横幅（banner）
		bannerMode(Banner.Mode.OFF);

		// 确保资源被复制过去
		this.application.addPrimarySources(this.sources);

		return child;
	}

	/**
	 * 添加一个父应用，使用提供的配置源。默认的参数和环境会复制到父应用，
	 * 但其他配置则是全新开始。
	 * @param sources 应用的配置源（Spring 配置）
	 * @return 父级构建器
	 */
	public SpringApplicationBuilder parent(Class<?>... sources) {
		if (this.parent == null) {
			this.parent = new SpringApplicationBuilder(sources).web(WebApplicationType.NONE)
					.properties(this.defaultProperties).environment(this.environment);
		}
		else {
			this.parent.sources(sources);
		}
		return this.parent;
	}

	private SpringApplicationBuilder runAndExtractParent(String... args) {
		if (this.context == null) {
			run(args);
		}
		if (this.parent != null) {
			return this.parent;
		}
		throw new IllegalStateException(
				"No parent defined yet (please use the other overloaded parent methods to set one)");
	}

	/**
	 * 将一个已经运行的父上下文添加到现有的应用中。
	 * @param parent 父上下文
	 * @return 当前的构建器（不是父构建器）
	 */
	public SpringApplicationBuilder parent(ConfigurableApplicationContext parent) {
		this.parent = new SpringApplicationBuilder();
		this.parent.context = parent;
		this.parent.running.set(true);
		return this;
	}

	/**
	 * 创建一个兄弟应用（拥有相同的父应用）。调用此方法的副作用是当前应用（及其父应用）
	 * 如果尚未运行，则会以无参数的方式启动它们。
	 * 如果想在启动当前应用及其父应用时传入参数，请使用 {@link #sibling(Class[], String...)} 方法。
	 * @param sources 应用的配置类（Spring 配置）
	 * @return 新的兄弟构建器
	 */
	public SpringApplicationBuilder sibling(Class<?>... sources) {
		return runAndExtractParent().child(sources);
	}

	/**
	 * 创建一个兄弟应用（拥有相同的父应用）。调用此方法的副作用是如果当前应用（及其父应用）
	 * 尚未运行，则会启动它们。
	 * @param sources 应用的配置类（Spring 配置）
	 * @param args 启动当前应用及其父应用时使用的命令行参数
	 * @return 新的兄弟构建器
	 */
	public SpringApplicationBuilder sibling(Class<?>[] sources, String... args) {
		return runAndExtractParent(args).child(sources);
	}

	/**
	 * 明确设置用于创建应用上下文的工厂。
	 * @param factory 要使用的工厂
	 * @return 当前构建器实例
	 * @since 2.4.0
	 */
	public SpringApplicationBuilder contextFactory(ApplicationContextFactory factory) {
		this.application.setApplicationContextFactory(factory);
		return this;
	}

	/**
	 * 向此应用添加更多来源（配置类和组件）。
	 * @param sources 要添加的来源
	 * @return 当前构建器实例
	 */
	public SpringApplicationBuilder sources(Class<?>... sources) {
		this.sources.addAll(new LinkedHashSet<>(Arrays.asList(sources)));
		return this;
	}

	/**
	 * 显式指定特定类型的 Web 应用。如果未设置，则根据类路径自动检测。
	 * @param webApplicationType Web 应用类型
	 * @return 当前构建器实例
	 * @since 2.0.0
	 */
	public SpringApplicationBuilder web(WebApplicationType webApplicationType) {
		this.application.setWebApplicationType(webApplicationType);
		return this;
	}

	/**
	 * 标识是否应记录启动信息。
	 * @param logStartupInfo 要设置的标志，默认值为 true。
	 * @return 当前构建器实例
	 */
	public SpringApplicationBuilder logStartupInfo(boolean logStartupInfo) {
		this.application.setLogStartupInfo(logStartupInfo);
		return this;
	}

	/**
	 * 设置用于打印横幅的 {@link Banner} 实例（当没有提供静态横幅文件时使用）。
	 * @param banner 要使用的横幅
	 * @return 当前的构建器实例
	 */
	public SpringApplicationBuilder banner(Banner banner) {
		this.application.setBanner(banner);
		return this;
	}

	public SpringApplicationBuilder bannerMode(Banner.Mode bannerMode) {
		this.application.setBannerMode(bannerMode);
		return this;
	}

	/**
	 * 设置应用是否为无头模式（headless），是否不实例化 AWT。默认值为 {@code true}，以防止出现 Java 图标。
	 * @param headless 是否为无头模式
	 * @return 当前的构建器实例
	 */
	public SpringApplicationBuilder headless(boolean headless) {
		this.application.setHeadless(headless);
		return this;
	}

	/**
	 * 设置创建的 {@link ApplicationContext} 是否应注册关闭钩子（shutdown hook）。
	 * @param registerShutdownHook 是否注册关闭钩子
	 * @return 当前的构建器实例
	 */
	public SpringApplicationBuilder registerShutdownHook(boolean registerShutdownHook) {
		this.registerShutdownHookApplied = true;
		this.application.setRegisterShutdownHook(registerShutdownHook);
		return this;
	}

	/**
	 * 固定用于启动信息锚点的主应用类。
	 * @param mainApplicationClass 要使用的类。
	 * @return 当前的构建器实例
	 */
	public SpringApplicationBuilder main(Class<?> mainApplicationClass) {
		this.application.setMainApplicationClass(mainApplicationClass);
		return this;
	}

	/**
	 * 标志是否将命令行参数添加到环境中。
	 * @param addCommandLineProperties 标志，默认值为 true。
	 * @return 当前的构建器实例
	 */
	public SpringApplicationBuilder addCommandLineProperties(boolean addCommandLineProperties) {
		this.application.setAddCommandLineProperties(addCommandLineProperties);
		return this;
	}

	/**
	 * 标记是否应将 {@link ApplicationConversionService} 添加到应用上下文的 {@link Environment} 中。
	 * @param addConversionService 是否添加转换服务。
	 * @return 当前的构建器实例
	 * @since 2.1.0
	 */
	public SpringApplicationBuilder setAddConversionService(boolean addConversionService) {
		this.application.setAddConversionService(addConversionService);
		return this;
	}

	/**
	 * 添加 {@link BootstrapRegistryInitializer} 实例，用于初始化 {@link BootstrapRegistry}。
	 * @param bootstrapRegistryInitializer 要添加的引导注册表初始化器
	 * @return 当前的构建器实例
	 * @since 2.4.5
	 */
	public SpringApplicationBuilder addBootstrapRegistryInitializer(
			BootstrapRegistryInitializer bootstrapRegistryInitializer) {
		this.application.addBootstrapRegistryInitializer(bootstrapRegistryInitializer);
		return this;
	}

	/**
	 * 控制应用是否应延迟初始化的标志。
	 * @param lazyInitialization 需要设置的标志。默认值为 false。
	 * @return 当前的构建器实例
	 * @since 2.2
	 */
	public SpringApplicationBuilder lazyInitialization(boolean lazyInitialization) {
		this.application.setLazyInitialization(lazyInitialization);
		return this;
	}

	/**
	 * 环境的默认属性，格式为 {@code key=value} 或 {@code key:value}。
	 * 多次调用此方法会累积属性，不会清除之前设置的属性。
	 * @param defaultProperties 要设置的属性
	 * @return 当前的构建器实例
	 * @see SpringApplicationBuilder#properties(Properties)
	 * @see SpringApplicationBuilder#properties(Map)
	 */
	public SpringApplicationBuilder properties(String... defaultProperties) {
		return properties(getMapFromKeyValuePairs(defaultProperties));
	}

	private Map<String, Object> getMapFromKeyValuePairs(String[] properties) {
		Map<String, Object> map = new HashMap<>();
		for (String property : properties) {
			int index = lowestIndexOf(property, ":", "=");
			String key = (index > 0) ? property.substring(0, index) : property;
			String value = (index > 0) ? property.substring(index + 1) : "";
			map.put(key, value);
		}
		return map;
	}

	private int lowestIndexOf(String property, String... candidates) {
		int index = -1;
		for (String candidate : candidates) {
			int candidateIndex = property.indexOf(candidate);
			if (candidateIndex > 0) {
				index = (index != -1) ? Math.min(index, candidateIndex) : candidateIndex;
			}
		}
		return index;
	}

	/**
	 * 环境的默认属性。多次调用此方法会累积属性，不会清除之前设置的属性。
	 * @param defaultProperties 要设置的属性
	 * @return 当前的构建器实例
	 * @see SpringApplicationBuilder#properties(String...)
	 * @see SpringApplicationBuilder#properties(Map)
	 */
	public SpringApplicationBuilder properties(Properties defaultProperties) {
		return properties(getMapFromProperties(defaultProperties));
	}

	private Map<String, Object> getMapFromProperties(Properties properties) {
		Map<String, Object> map = new HashMap<>();
		for (Object key : Collections.list(properties.propertyNames())) {
			map.put((String) key, properties.get(key));
		}
		return map;
	}

	/**
	 * 环境的默认属性。多次调用此方法会累积属性，不会清除之前设置的属性。
	 * @param defaults 默认属性
	 * @return 当前的构建器实例
	 * @see SpringApplicationBuilder#properties(String...)
	 * @see SpringApplicationBuilder#properties(Properties)
	 */
	public SpringApplicationBuilder properties(Map<String, Object> defaults) {
		this.defaultProperties.putAll(defaults);
		this.application.setDefaultProperties(this.defaultProperties);
		if (this.parent != null) {
			this.parent.properties(this.defaultProperties);
			this.parent.environment(this.environment);
		}
		return this;
	}

	/**
	 * 添加当前应用（及其父应用和子应用）的激活 Spring 配置文件。
	 * @param profiles 要添加的配置文件
	 * @return 当前的构建器实例
	 */
	public SpringApplicationBuilder profiles(String... profiles) {
		this.additionalProfiles.addAll(Arrays.asList(profiles));
		this.application.setAdditionalProfiles(StringUtils.toStringArray(this.additionalProfiles));
		return this;
	}

	private SpringApplicationBuilder additionalProfiles(Collection<String> additionalProfiles) {
		this.additionalProfiles = new LinkedHashSet<>(additionalProfiles);
		this.application.setAdditionalProfiles(StringUtils.toStringArray(this.additionalProfiles));
		return this;
	}

	/**
	 * 用于应用上下文中自动生成 Bean 名称的生成器。
	 * @param beanNameGenerator 要设置的生成器
	 * @return 当前的构建器实例
	 */
	public SpringApplicationBuilder beanNameGenerator(BeanNameGenerator beanNameGenerator) {
		this.application.setBeanNameGenerator(beanNameGenerator);
		return this;
	}

	/**
	 * 应用上下文的环境配置。
	 * @param environment 要设置的环境
	 * @return 当前的构建器实例
	 */
	public SpringApplicationBuilder environment(ConfigurableEnvironment environment) {
		this.application.setEnvironment(environment);
		this.environment = environment;
		return this;
	}

	/**
	 * 从系统环境中获取配置属性时应应用的前缀。
	 * @param environmentPrefix 要设置的环境属性前缀
	 * @return 当前的构建器实例
	 * @since 2.5.0
	 */
	public SpringApplicationBuilder environmentPrefix(String environmentPrefix) {
		this.application.setEnvironmentPrefix(environmentPrefix);
		return this;
	}

	/**
	 * 应用上下文的资源加载器。如果需要自定义类加载器，可在此处添加。
	 * @param resourceLoader 要设置的资源加载器
	 * @return 当前的构建器实例
	 */
	public SpringApplicationBuilder resourceLoader(ResourceLoader resourceLoader) {
		this.application.setResourceLoader(resourceLoader);
		return this;
	}

	/**
	 * 添加一些初始化器到应用（在任何 Bean 定义加载之前应用到 {@link ApplicationContext}）。
	 * @param initializers 需要添加的一些初始化器
	 * @return 当前的构建器实例
	 */
	public SpringApplicationBuilder initializers(ApplicationContextInitializer<?>... initializers) {
		this.application.addInitializers(initializers);
		return this;
	}

	/**
	 * 添加一些监听器到应用（监听 SpringApplication 事件以及上下文运行后普通的 Spring 事件）。
	 * 任何同时实现了 {@link ApplicationContextInitializer} 的监听器将自动加入
	 * {@link #initializers(ApplicationContextInitializer...) initializers} 中。
	 * @param listeners 需要添加的一些监听器
	 * @return 当前的构建器实例
	 */
	public SpringApplicationBuilder listeners(ApplicationListener<?>... listeners) {
		this.application.addListeners(listeners);
		return this;
	}

	/**
	 * 配置用于 {@link ApplicationContext} 的 {@link ApplicationStartup}，以收集启动指标。
	 * @param applicationStartup 要使用的应用启动收集器
	 * @return 当前的构建器实例
	 * @since 2.4.0
	 */
	public SpringApplicationBuilder applicationStartup(ApplicationStartup applicationStartup) {
		this.application.setApplicationStartup(applicationStartup);
		return this;
	}

	/**
	 * 是否允许 Bean 之间的循环依赖，并自动尝试解决它们。
	 * @param allowCircularReferences 是否允许循环依赖
	 * @return 当前的构建器实例
	 * @since 2.6.0
	 * @see AbstractAutowireCapableBeanFactory#setAllowCircularReferences(boolean)
	 */
	public SpringApplicationBuilder allowCircularReferences(boolean allowCircularReferences) {
		this.application.setAllowCircularReferences(allowCircularReferences);
		return this;
	}

}
